/*
 * Copyright 2009-2010 Nanjing RedOrange ltd (http://www.red-orange.cn)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package redora.api.fetch;

import java.util.Date;

import redora.exceptions.PagingException;

import static redora.api.fetch.Mode.All;
import static redora.api.fetch.Mode.Scroll;
import static redora.api.fetch.Scope.Form;
import static redora.api.fetch.Scope.List;
import static redora.api.fetch.Scope.Table;

/**
 * Page is the carrier for paginated queries. Redora sees pagination in two
 * dimensions. First is the pagination itself. We call it the pagination Mode.
 * If you want to retrieve all the records, choose Mode.All. For real pagination
 * choose Mode.Page or Mode.Scroll.<br>
 * The other dimension is about the columns do you wish to retrieve. We call
 * that pagination Scope. For example if you wish to retrieve the records to
 * show them in a table, choose Scope.Table.<br>
 * <p/>
 * The Page object itself is the carrier for paginated queries. For example you
 * want a query to be paginated in pages with 10 results. First initiated the
 * Page object:<br>
 * <p/>
 * <pre>
 * Page page = new Page(Scope.Table, Mode.Page, 10);
 * </pre>
 * <p/>
 * Then run the query:<br>
 * <p/>
 * <pre>
 * List&lt;MyObject&gt; objects = service.findAll(page);
 * </pre>
 * <p/>
 * Now your service class will first retrieve the pagination information. That
 * is a "select count(1)" query to determine how many records this query will
 * retrieve (resultCount). Then it will fetch the first page of pageSize
 * records. So for example you want to page through all the records:<br>
 * <p/>
 * <pre>
 * Page page = new Page(Scope.Table, Mode.Page, 10);
 * while (page.hasNext()) {
 *     List&lt;MyObject&gt; objects = service.findAll(page.next());
 *     //Do some stuff
 * }
 * </pre>
 * <p/>
 * There are problably many occasions you do not wat to paginate. But the
 * service interface does ask for a Page object. In those case use one of the
 * pre-defined Page objects: ALL_TABLE, ALL_LIST and ALL_FORM. Like:<br>
 * <p/>
 * <pre>
 * List&lt;MyObject&gt; objects = service.findAll(Page.ALL_LIST);
 * </pre>
 * <p/>
 * The Page object will hold the pagination information, so you can show this to
 * the user. When you run Mode.Page, you can show something familiar like:<br>
 * first previous 1 2 3 ... 11 12 next last<br>
 * The Page class will have handles for each of these: first(), previous(),
 * goTo(), next() and last(). With pageCount() and position() you can create the
 * 1, 2, 3... widget.<br>
 * <p/>
 * A different approach is Mode.Scroll. In this case you just walk through the
 * results until there are no records left. So, the only usable interface is
 * next() and hasNext(). The example is almost identical to Mode.Page:<br>
 * <p/>
 * <pre>
 * Page page = new Page(Scope.Table, Mode.Scroll, 10);
 * while (page.hasNext()) {
 *     List&lt;MyObject&gt; objects = service.findAll(page.next());
 *     //Do some stuff
 * }
 * </pre>
 * <p/>
 * However, when you for example try last() or goTo() you will get a
 * PagingException. Under water the big difference is, there is no statistical
 * select count(1) query, so the Page object in Scroll mode does not know how
 * many pages there are. The hasNext() handle is not 100% deterministic, it will
 * try next() until it is sure there are no more records. That means: or the
 * last page returned less then pageSize records, or next() returned nothing.<br>
 * You typically use this in an AJAX enable scrollable table (the scroll down
 * will lead to under water fill up requests) or as a scalable way to process
 * all the records in a J2SE batch job.<br>
 * <p/>
 * You most probably want to use pagniation in a (AJAX enabled) client. For this
 * you can use the fromJSPON and toJSON function. Typically a response will have
 * the page object in JSON.<br>
 * <p/>
 * NOTE: You already know the hasNext() function in Mode.Scroll is not perfect.
 * Remember that while you are traversing pages things can change. So, if new
 * records are added or records are removed, the results can be undetermined.
 * For example a hasNext()/next() can result in zero results while you had
 * expected results. Or in a next() page a record shown in the previous page can
 * appear again! If this is more or less important you can evaluate age() or
 * birth(). You can consider using a Set to avoid the display of processing of
 * re-appearing records.
 *
 * @see Scope
 * @see Mode
 */
public class Page {
    private final Scope scope;
    private final Mode mode;

    private int position = -1;
    private int resultCount = -1;
    private int pageSize = -1;
    private final Date birth;

    /**
     * Paging disabled Page (Mode.All).
     */
    private Page(Scope scope) {
        this.scope = scope;
        this.mode = All;
        resultCount = -1;
        position = 0;
        birth = new Date();
    }

    public Page(Scope scope, Mode mode, int pageSize) {
        this.scope = scope;
        this.mode = mode;
        this.pageSize = pageSize;
        position = -1;
        resultCount = -1;
        birth = new Date();
    }

    /**
     * The service sets Page parameters after retrieving the data with a first
     * query.
     */
    public void initPageMode(int resultCount) throws PagingException {
        if (mode != Mode.Page) {
            throw new PagingException("Invalid mode to set" + "Paging data "
                    + mode);
        }
        if (this.resultCount != -1) {
            throw new PagingException("This page is already initialized.");
        }
        this.resultCount = resultCount;
        this.position = 0;
    }

    /**
     * The service sets Page parameters for Scroll mode.
     *
     * @throws PagingException When the Page object is used in other then Scroll mode.
     */
    public void initScrollMode() throws PagingException {
        if (mode != Scroll) {
            throw new PagingException("Invalid mode to set Paging data " + mode);
        }
        this.position = 0;
    }

    /**
     * Commonly used Page: returns all records in Table scope.
     */
    public final static Page ALL_TABLE = new Page(Table);
    /**
     * Commonly used Page: returns all records in List scope.
     */
    public final static Page ALL_LIST = new Page(List);
    /**
     * Commonly used Page: returns all records in Form scope.
     */
    public final static Page ALL_FORM = new Page(Form);

    public int pageCount() throws PagingException {
        if (resultCount != -1) {
            if (resultCount % pageSize == 0) {
                return resultCount / pageSize;
            } else {
                return resultCount / pageSize + 1;
            }
        }
        if (mode != Mode.Page) {
            throw new PagingException("PageCount is"
                    + "only available for Mode.Page pagination.");
        }
        throw new PagingException("First retrieve the first Page,"
                + "then perform any other of the operations.");
    }

    public int resultCount() {
        return resultCount;
    }

    public int pageSize() {
        return pageSize;
    }

    public int position() throws PagingException {
        if (mode == Mode.Page) {
            return position;
        }
        throw new PagingException(
                "Position is only available for Mode.Page pagination");
    }

    public Page next() throws PagingException {
        if (mode == All) {
            throw new PagingException("Paging is not available,"
                    + "you have retrieved already all records.");
        } else if (!hasNext()) {
            throw new PagingException("The current position " + position
                    + " already has reached it's maximum."
                    + "use hasNext() to determine this"
                    + "the next time to avoid this exception.");
        }
        position++;

        return this;
    }

    public boolean hasNext() throws PagingException {
        if (position == -1) {
            return true;
        } else if (mode == All) {
            return false;
        }
        return position < pageCount() - 1;
    }

    /**
     * @return The Page object set at the previous page
     * @throws PagingException When the mode is not suitable for paging or when there is no
     *                         previous page.
     */
    public Page previous() throws PagingException {
        if (mode == All) {
            throw new PagingException("Paging is not available,"
                    + "you have retrieved already all records.");
        } else if (position == -1) {
            throw new PagingException("First retrieve the first Page,"
                    + "then perform any other operation.");
        } else if (position == 0) {
            throw new PagingException("You are at the first position,"
                    + "you can not goto the previous page.");
        }
        position--;
        return this;
    }

    public Page first() throws PagingException {
        if (mode == All) {
            throw new PagingException("Paging is not available, you have"
                    + "retrieved already all records.");
        } else if (position == 0) {
            throw new PagingException("You are already at the first position");
        }
        position = 0;
        return this;
    }

    public Page last() throws PagingException {
        if (mode == All) {
            throw new PagingException("Paging is not available,"
                    + "you have retrieved already all records.");
        } else if (!hasNext()) {
            throw new PagingException("The current position " + position
                    + " already has reached it's maximum."
                    + "use hasNext() to determine this"
                    + "the next time to avoid this exception.");
        }
        position = pageCount() - 1;
        return this;
    }

    public Page goTo(int position) throws PagingException {
        if (mode == All) {
            throw new PagingException("Paging is not available,"
                    + "you have retrieved already all records.");
        } else if (position == -1) {
            throw new PagingException("First retrieve the first Page,"
                    + "then perform any other operation.");
        } else if (position > pageCount() - 1) {
            throw new PagingException("The requested position " + position
                    + " exceeds the page count " + pageCount());
        }
        this.position = position;
        return this;
    }

    public Mode getMode() {
        return mode;
    }

    public Scope getScope() {
        return scope;
    }

    /**
     * create date/time of this Page object.
     */
    public Date birth() {
        return birth;
    }

    /**
     * Time in milliseconds after the initialization of this Page object.
     */
    public long age() {
        return new Date().getTime() - birth.getTime();
    }

    public static Page fromJSON(String stream) {
        Page page;
        String[] strs = stream.split(",");
        String mode = null;
        String scope = null;
        int pageSize = -1;
        int resultCount = -1;
        int position = -1;
        for (String s : strs) {
            String[] temp = s.split(":");
            if (temp[0].equals("mode")) {
                mode = temp[1];
            }
            if (temp[0].equals("scope")) {
                scope = temp[1];
            }
            if (temp[0].equals("pageSize")) {
                pageSize = Integer.parseInt(temp[1]);
            }
            if (temp[0].equals("resultCount")) {
                resultCount = Integer.parseInt(temp[1]);
            }
            if (temp[0].equals("position")) {
                position = Integer.parseInt(temp[1]);
            }
        }
        if (All.name().equalsIgnoreCase(mode)) {
            if (Form.name().equalsIgnoreCase(scope))
                return Page.ALL_FORM;
            else if (Form.name().equalsIgnoreCase(scope))
                return Page.ALL_TABLE;
            else if (Form.name().equalsIgnoreCase(scope))
                return Page.ALL_LIST;
            return null;
        } else if (Scroll.name().equalsIgnoreCase(mode)) {
            page = new Page(Scope.valueOf(scope), Scroll, pageSize);
            page.resultCount = resultCount;
        } else if (Mode.Page.name().equalsIgnoreCase(mode)) {
            page = new Page(Scope.valueOf(scope), Mode.Page, pageSize);
            page.resultCount = resultCount;
            page.position = position;
        } else {
            page = null;
        }
        return page;
    }

    public String toJSON() throws PagingException {
        StringBuilder s = new StringBuilder("mode:").append("\"");
        switch (mode) {
            case All:
                s.append(All.name()).append("\"")
                        .append(",scope:").append("\"").append(this.scope.name())
                        .append("\"");
                break;
            case Scroll:
                s.append(Scroll.name()).append(
                        "\"").append(",scope:").append("\"").append(
                        this.scope.name()).append("\"").append("resultCount:")
                        .append(resultCount).append(",pageSize:").append(pageSize);
                break;
            case Page:
                s.append(Mode.Page.name())
                        .append("\"").append(",scope:").append("\"").append(
                        this.scope.name()).append("\"").append(
                        ",resultCount:").append(resultCount).append(
                        ",pageSize:").append(pageSize)
                        .append(",pageCount:").append(pageCount()).append(
                        ",position:").append(position);
                break;
            default:
                throw new IllegalArgumentException("Undefined Mode " + mode);
        }

        return s.toString();
    }
}
